BigDb.php

<?php

namespace Tlf;

/**
 * Extend this class to create an interface for your BigDb library. Provides many convenience methods for various features. Uses 4 traits. 
 */
class BigDb {

    /** for convenience sql commands like select, insert, update, delete */
    use BigDb\SqlVerbs;
    /** To easily load sql from .sql files */
    use BigDb\SqlFiles;
    /** Integrate with LilDb's LilMigrations class, to handle database versioning */
    use BigDb\Migrations;
    /** For loading of ORMs */
    use BigDb\GetOrms;

    public readonly \PDO $pdo;
    public \Tlf\LilDb $ldb;
    /** Sql queries, typically from .sql files on disk */
    protected array $sql;
    /** The namespace from which Orm classes should be loaded */
    protected string $orm_namespace;
    /** The name used by BigDbServer to identify this BigDb instance. Does NOT correspond to a mysql database. Also, {}@see get_db_name()} */
    protected string $db_name;

    public string $root_dir;

    /** 
     * The application's timezone as a DateTimeZone-compatible string, like 'America/Chicago' or 'UTC'
     *
     * All DateTimes are converted to UTC for database storage bc MySql does not store timezone information.
     */
    public string $timezone = 'UTC';


    /**
     * Construct a BigDb instance and initialize it.
     *
     * @param $pdo \PDO a pdo instance with a valid database connection
     * @param $root_dir ?string directory of the database app to load. If null, then uses `$this->get_root_dir()`
     */
    public function __construct(\PDO $pdo, ?string $root_dir = null){
        $this->pdo = $pdo;
        $this->ldb = new \Tlf\LilDb($pdo);
        $this->root_dir = $root_dir ?? $this->get_root_dir();

        $this->init_sql();
    }
    /**
     * Initialize the sql statements from the compiled sql file, only if `$this->sql` is not set
     *
     * @return void
     *
     * @override to use a different sql storage system than LilDb's LilSql
     */
    protected function init_sql(){
        if (isset($this->sql))return;
        $this->load_queries($this->get_root_dir().'/sql/');
    
    }


    /**
     * Get the stored sql queries
     */
    public function getSql(): array{
        return $this->sql;
    }


    /**
     * Get path to the root of a BigDb library. 
     *
     * @return directory name where your BigDb subclass is defined, or null if not a subclass
     * @override if your BigDb subclass is not in the root directory of your bigdb library. 
     */
    public function get_root_dir(): ?string {
        if (isset($this->root_dir))return $this->root_dir;
        if (!is_subclass_of($this, self::class)){
            throw new \Exception("You must set root_dir on your db instance");
            return null;
        }
        $refClass = new \ReflectionClass($this);
        $file = $refClass->getFileName();
        $dir = dirname($file);
        return $dir;
    }


    /**
     * Get a name for the BigDbServer to reference.
     * @return string a string name, typically snake_case. Default implementation returns `$this->db_name` if set, or a snake_case version of the class's basename if `protected $db_name` is not set.
     *
     * @override if setting `$this->db_name` will not work for you AND you don't want a snake_case version of the class's basename.
     */
    public function get_db_name(): string {
        if (isset($this->db_name))return $this->db_name;
        $class_name = get_class($this);
        $parts = explode('\\', $class_name);
        $base = array_pop($parts);
        $snake = preg_replace('/([a-z])([A-Z])([a-z])/', '$1_$2$3', $base);
        $snake = strtolower($snake);
        return $snake;
    }

    /**
     * Convert an array of orms to an array of arrays
     * @param $orms_array `array<int index, \Tlf\BigOrm>` items to convert to arrays
     *
     * @return array<int index, array db_row>
     */
    public function to_arrays(array $orms_array): array{
        return array_map(
            function(\Tlf\BigOrm $v){
                return $v->get_db_row();
            }, $orms_array
        );
    }


    /**
     * Coerce a property value to a database value
     *
     * @param $type string type, from ReflectionProperty
     * @param $property_value mixed value of the given $type
     * @param $property_name string property name for some automatic-conversions like uuid, or empty string
     *
     * @return a database-friendly value
     * @throw \Exception if value cannot be coerced
     */
    public function coerce_to_db(string $type, mixed $property_value, string $property_name=''): mixed {
        if ($type[0]=='?')$type = substr($type,1);
        switch ($type) {
            case "bool":
                return $property_value ? 1 : 0;
            case "DateTime":
                $property_value->setTimezone(new \DateTimeZone("UTC"));
                return $property_value->format('Y-m-d H:i:s');
        }

        $combo = $type.'_'.$property_name;
        switch ($combo){
            case "string_uuid":
                if ($property_value==null)return null;
                return $this->uuid_to_bin($property_value);

        }

        if ($property_value instanceof \BackedEnum){
            return $property_value->value;
        }

        throw new \Exception("Cannot coerce property '$property_name' of type '$type' to db value.");
    }

    /**
     * Coerce a database value into a property value
     *
     * @param $type string type, from ReflectionProperty
     * @param $db_value mixed value as retrieved from database
     * @param $property_name string property name for some automatic-conversions like uuid, or empty string
     *
     * @return mixed value of the given $type
     * @throw \Exception if value cannot be coerced
     */
    public function coerce_from_db(string $type, mixed $db_value, string $property_name): mixed {
        if ($type[0]=='?')$type = substr($type,1);
        switch ($type) {
            case "bool":
                return (bool)$db_value;
            case "DateTime":
                $dt = \DateTime::createFromFormat('Y-m-d H:i:s', $db_value);
                if ($dt === false)throw new \Exception("Datetime string '$db_value' cannot be automatically instantiated as a DateTime object. Implement a manual datetime conversion, or fix the format.\n");
                $dt->setTimezone(new \DateTimeZone($this->timezone));
                return $dt;
        }

        $combo = $type.'_'.$property_name;
        switch ($combo){
            case "string_uuid":
                if ($db_value==null)return null;
                return $this->bin_to_uuid($db_value);

        }

        if (is_a($type, 'BackedEnum',true)){
            return $type::from($db_value);
        }

        throw new \Exception("Cannot coerce property '$property_name' of type '$type' to db value.");
    }


    /**
     * Convert binary uuid to a string uuid (mysql compatible). 
     * @param $uuid a binary(16) uuid from MYSQL created via `UUID_TO_BIN( UUID() )`
     * @return string a VARCHAR(36) compatible $uuid identical to `BIN_TO_UUID( binary_16_representation_of_uuid )`
     */
    public function bin_to_uuid(string $uuid): string{
        $hex = str_split(bin2hex($uuid), 4);

        return 
            $hex[0].$hex[1].'-'
            .$hex[2].'-'
            .$hex[3].'-'
            .$hex[4].'-'
            .$hex[5].$hex[6].$hex[7]
        ;
    }

    /**
     * Convert a string uuid to a binary uuid (mysql compatible).
     * @param $uuid a VARCHAR(36) representation of a UUID, generated in MySql with `UUID()`
     * @return string a BINARY(16) representation of a UUID, generated in MySql with `BIN_TO_UUID( UUID() )`
     */
    public function uuid_to_bin(string $uuid): string {
        return hex2bin(
            str_replace('-','', $uuid)
        );
    }
}